﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Linq.Expressions;
using System.Globalization;
using System.Reflection.Emit;
using System.Runtime.Remoting.Activation;
using System.Security.Permissions;

namespace SwiftMVVM.ViewModel
{
    /// <summary>
    /// Trying a different approach here to see if we can 
    /// see a performance gain over Activator.CreateInstance
    /// </summary>
    internal static class TypeFactory
    {
        /// <summary>
        /// We're going to store the creator functions based on the Type and constructor arguments
        /// It's a small cost to cache them so we only have to pay the cost of creating them once.
        /// </summary>
        private static readonly Dictionary<string, Func<object[], object>> creationFunctions =
            new Dictionary<string, Func<object[], object>>();

        /// <summary>
        /// Lock Object
        /// </summary>
        private static readonly object lockObject = new object();

        /// <summary>
        /// Creates an instance of the specified type using the constructor that best
        //  matches the specified parameters. 
        /// </summary>
        /// <param name="type">The type of object to create.</param>
        /// <param name="args">An array of arguments that match in number, order, and type the parameters
        /// of the constructor to invoke. If args is an empty array or null, the constructor
        /// that takes no parameters (the default constructor) is invoked.</param>
        /// <returns>A reference to the newly created object.</returns>
        public static object CreateInstance(Type type, params object[] args)
        {
            if (args == null || args.Length == 0)
            {
                //No real benefit here when it comes to us 
                //caching the create function
                return Activator.CreateInstance(type);
            }

            //We create a key based on the Type and the arguments passed to it.
            var typeArgumentKey = string.Concat(type.Name, string.Join("|", args.
                Select(n => n == null ? "null" : n.GetType().Name).ToArray()));

            var createFunction =
                creationFunctions.GetOrAdd(lockObject, typeArgumentKey, key => GetAllocator(type, args));

            //Invoke the delegate and return the result
            return createFunction(args);
        }

        /// <summary>
        /// Creates an instance of the type designated by the specified generic type
        /// parameter, using the parameterless constructor .
        /// </summary>
        /// <typeparam name="T">The type to create</typeparam>
        /// <returns>A reference to the newly created object.</returns>
        public static T CreateInstance<T>() where T : class
        {
            return CreateInstance(typeof(T)) as T;
        }

        private static Func<object[], object> GetAllocatorImpl(ConstructorInfo constructorInfo, params Type[] argumentTypes)
        {
            var argumentExpression = Expression.Parameter(typeof(object[]), "");

            //Generate the actual creation function
            return Expression.Lambda<Func<object[], object>>(
                    Expression.New(constructorInfo, argumentTypes.Select((to, ti) =>
                        Expression.Convert(Expression.Coalesce(Expression.ArrayIndex(argumentExpression,
                        Expression.Constant(ti)), Expression.Call(typeof(TypeFactory),
                        "Default", new Type[] { to }, null)), to)).ToArray()), argumentExpression).Compile();
        }

        public static Func<object[], object> GetAllocatorExact<T>(params Type[] argumentTypes)
        {
            return GetAllocatorImpl(typeof(T).GetConstructor(argumentTypes), argumentTypes);
        }

        public static Func<object[], object> GetAllocatorExact(Type type, params Type[] argumentTypes)
        {
            return GetAllocatorImpl(type.GetConstructor(argumentTypes), argumentTypes);
        }

        /// <summary>
        /// Examines the type passed in, along with it's arguments and generates
        /// a creation function that can be used to directly instantiate the object.
        /// This approach seems to take approx 1/3 the time of Activator.CreateInstance
        /// </summary>
        /// <param name="type">The type to instantiate</param>
        /// <param name="args">The arguments to pass to the constructor</param>
        /// <returns>A method that can be used to instantiate the type/argument combination</returns>
        public static Func<object[], object> GetAllocator(Type type, params object[] args)
        {
            if (args == null || args.Length == 0)
            {
                throw new ArgumentOutOfRangeException(
                    "Empty args, call Activator.CreateInstance directly, it's faster for that case");
            }

            //Determine the appropriate constructor to use for this Type/Argument combination
            //We're doing our best here to replicate the logic that Activator.CreateInstance 
            //uses to figure out the right constructorinfo. We want to be consistant here
            var constructorInfo = GetConstructorInfo(type,
                BindingFlags.CreateInstance | BindingFlags.Public | BindingFlags.Instance,
                null, args, null, null);

            //Grab the argument types for the constructor we matched
            var argumentTypes = constructorInfo.GetParameters().Select(pi => pi.ParameterType).ToArray();

            return GetAllocatorImpl(constructorInfo, argumentTypes);
        }

        /// <summary>
        /// TO BE COMMENTED (and cleaned up), but the short of
        /// it is that everything below this is essentially from Activator.CreateInstace's
        /// ConstructorInfo discovery procedure.
        /// </summary>
        private static ConstructorInfo GetConstructorInfo(Type type, BindingFlags bindingAttr, Binder binder, object[] args, CultureInfo culture, object[] activationAttributes)
        {
            if (type == null) { throw new ArgumentNullException("type"); }

            if (type is TypeBuilder) { throw new NotSupportedException("NotSupported_CreateInstanceWithTypeBuilder"); }

            if ((((int)bindingAttr) & 0xff) == (int)BindingFlags.Default)
            {
                bindingAttr |= BindingFlags.CreateInstance | BindingFlags.Public | BindingFlags.Instance;
            }

            if ((activationAttributes != null) && (activationAttributes.Length > 0))
            {
                if (!type.IsMarshalByRef)
                {
                    throw new NotSupportedException("NotSupported_ActivAttrOnNonMBR");
                }
                if (!type.IsContextful && ((activationAttributes.Length > 1) || !(activationAttributes[0] is UrlAttribute)))
                {
                    throw new NotSupportedException("NotSupported_NonUrlAttrOnMBR");
                }
            }

            ConstructorInfo constructorInfo = null;
            MethodBase methodBase;
            if (args == null)
            {
                args = new object[0];
            }
            int length = args.Length;
            if (binder == null)
            {
                binder = Type.DefaultBinder;
            }
            if ((((length == 0) && ((bindingAttr & BindingFlags.Public) != BindingFlags.Default))
                && ((bindingAttr & BindingFlags.Instance) != BindingFlags.Default))
                && (type.IsCOMObject || type.IsSubclassOf(typeof(ValueType))))
            {
                return type.GetConstructor(Type.EmptyTypes);
            }

            var constructors = type.GetConstructors(bindingAttr);
            var list = new List<MethodBase>(constructors.Length);
            var argumentTypes = new Type[length];
            for (int i = 0; i < length; i++)
            {
                if (args[i] != null)
                {
                    argumentTypes[i] = args[i].GetType();
                }
            }

            for (int j = 0; j < constructors.Length; j++)
            {
                MethodBase base1 = constructors[j];
                if (FilterApplyMethodBaseInfo(constructors[j], bindingAttr, null, CallingConventions.Any, argumentTypes, false))
                {
                    list.Add(constructors[j]);
                }
            }

            MethodBase[] array = new MethodBase[list.Count];
            list.CopyTo(array);
            if ((array != null) && (array.Length == 0))
            {
                array = null;
            }
            if (array == null)
            {
                throw new MissingMethodException(string.Format(CultureInfo.CurrentCulture, "MissingConstructor_Name",
                    new object[] { type.FullName }));
            }
            if (((length == 0) && (array.Length == 1)) && ((bindingAttr & BindingFlags.OptionalParamBinding) == BindingFlags.Default))
            {
                return type.GetConstructor(Type.EmptyTypes);
            }

            object state = null;
            try
            {
                methodBase = binder.BindToMethod(bindingAttr, array, ref args, null, culture, null, out state);
            }
            catch (MissingMethodException)
            {
                methodBase = null;
            }

            if (methodBase == null)
            {
                throw new MissingMethodException(string.Format(CultureInfo.CurrentCulture, "MissingConstructor_Name",
                    new object[] { type.FullName }));
            }

            if (typeof(Delegate).IsAssignableFrom(methodBase.DeclaringType))
            {
                new SecurityPermission(SecurityPermissionFlag.UnmanagedCode).Demand();
            }

            constructorInfo = ((ConstructorInfo)methodBase);
            if (state != null)
            {
                binder.ReorderArgumentArray(ref args, state);
            }
            return constructorInfo;
        }

        private static bool FilterApplyMethodBaseInfo(MethodBase methodBase, BindingFlags bindingFlags,
            string name, CallingConventions callConv, Type[] argumentTypes, bool prefixLookup)
        {
            BindingFlags flags = bindingFlags;
            bindingFlags ^= BindingFlags.DeclaredOnly;

            return ((((bindingFlags & flags) == flags) && (!prefixLookup ||
                FilterApplyPrefixLookup(methodBase, name, (bindingFlags & BindingFlags.IgnoreCase) != BindingFlags.Default))) &&
                FilterApplyMethodBaseInfo(methodBase, bindingFlags, callConv, argumentTypes));
        }

        private static bool FilterApplyPrefixLookup(MemberInfo memberInfo, string name, bool ignoreCase)
        {
            if (ignoreCase)
            {
                if (!memberInfo.Name.ToLower(CultureInfo.InvariantCulture).StartsWith(name, StringComparison.Ordinal))
                {
                    return false;
                }
            }
            else if (!memberInfo.Name.StartsWith(name, StringComparison.Ordinal))
            {
                return false;
            }
            return true;
        }

        private static bool FilterApplyMethodBaseInfo(MethodBase methodBase, BindingFlags bindingFlags, CallingConventions callConv, Type[] argumentTypes)
        {
            if ((callConv & CallingConventions.Any) == 0)
            {
                if (((callConv & CallingConventions.VarArgs) != 0) && ((methodBase.CallingConvention & CallingConventions.VarArgs) == 0))
                {
                    return false;
                }
                if (((callConv & CallingConventions.Standard) != 0) && ((methodBase.CallingConvention & CallingConventions.Standard) == 0))
                {
                    return false;
                }
            }
            if (argumentTypes != null)
            {
                ParameterInfo[] parametersNoCopy = methodBase.GetParameters();
                if (argumentTypes.Length != parametersNoCopy.Length)
                {
                    if ((bindingFlags & (BindingFlags.SetProperty | BindingFlags.GetProperty | BindingFlags.CreateInstance | BindingFlags.InvokeMethod)) == BindingFlags.Default)
                    {
                        return false;
                    }
                    bool flag = false;
                    if (argumentTypes.Length > parametersNoCopy.Length)
                    {
                        if ((methodBase.CallingConvention & CallingConventions.VarArgs) == 0)
                        {
                            flag = true;
                        }
                    }
                    else if ((bindingFlags & BindingFlags.OptionalParamBinding) == BindingFlags.Default)
                    {
                        flag = true;
                    }
                    else if (!parametersNoCopy[argumentTypes.Length].IsOptional)
                    {
                        flag = true;
                    }
                    if (flag)
                    {
                        if (parametersNoCopy.Length != 0)
                        {
                            if (argumentTypes.Length < (parametersNoCopy.Length - 1))
                            {
                                return false;
                            }

                            var info = parametersNoCopy[parametersNoCopy.Length - 1];
                            if (!info.ParameterType.IsArray)
                            {
                                return false;
                            }

                            if (info.IsDefined(typeof(ParamArrayAttribute), false))
                            {
                                return true;
                            }
                        }
                        return false;
                    }
                }
                else if (((bindingFlags & BindingFlags.ExactBinding) != BindingFlags.Default) && ((bindingFlags & BindingFlags.InvokeMethod) == BindingFlags.Default))
                {
                    for (int i = 0; i < parametersNoCopy.Length; i++)
                    {
                        if ((argumentTypes[i] != null) && (parametersNoCopy[i].ParameterType != argumentTypes[i]))
                        {
                            return false;
                        }
                    }
                }
            }
            return true;
        }
    }
}
